/*
 * VoJxtaCallControl.java
 *
 * Created on March 3, 2005, 3:38 AM
 */

package net.jxta.myjxta.plugins.vojxta;

import net.jxta.endpoint.ByteArrayMessageElement;
import net.jxta.endpoint.MessageElement;
import net.jxta.endpoint.StringMessageElement;
import net.jxta.logging.Logging;
import net.jxta.myjxta.MyJXTA;
import net.jxta.myjxta.View;
import net.jxta.myjxta.dialog.Dialog;
import net.jxta.myjxta.dialog.DialogMessage;

import java.util.Date;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * @author Jeff Moore
 * @version .1
 * @modified 2005-03-28 jamoore : add call control protocol
 * @modified 2005-03-03 jamoore : add audio control accessors
 * @modified 2005-03-10 jamoore : add hold, resume to protocol
 * @modified 2005-03-14 jamoore : add quality map for user selectable quality
 * @modified 2005-03-15 jamoore : add quality element to start session command
 * @modified 2005-03-16 jamoore : add accessors for statistics
 * @modified 2005-03-26 jamoore : add MessageAckThread, ReceivedMessageTimeout thread
 */
public final class VoJxtaCallControl {

    /**
     * Time to wait for response after a command (not voice) message is sent
     * out. Default is 10 seconds.
     */
    private static final long DEFAULT_MESSAGE_ACK_TIMEOUT = 20000;

    /**
     * Minimum time we will wait till a message ack comes back
     */
    private static final long MINIMUM_MESSAGE_ACK_TIMEOUT = 2000;

    /**
     * Tag for an element element carrying voice data
     */
    public static final String TAG_VOICE_DATA = "VoiceData";

    /**
     * Element tag denoting a message containing vojxta session command data
     */
    public static final String TAG_SESSION_COMMAND = "VoJxtaSessionCommand";

    public static final String TAG_QUALITY_ELEMENT = "VoJxtaVoiceQuality";

    /**
     * commands sent btn peers managing the vojxta session
     */

    public static final String COMMAND_VOJXTA_INVITE_REQUEST = "VoJxtaInviteRequest";

    public static final String COMMAND_VOJXTA_INVITE_ACCEPT = "VoJxtaInviteAccept";

    public static final String COMMAND_VOJXTA_START_REQUEST = "VoJxtaStartRequest";

    public static final String COMMAND_VOJXTA_START_ACCEPT = "VoJxtaStartAccept";

    public static final String COMMAND_VOJXTA_DATA = "VoJxtaData";

    public static final String COMMAND_VOJXTA_HOLD_REQUEST = "VoJxtaHoldRequest";

    public static final String COMMAND_VOJXTA_HOLD_ACCEPT = "VoJxtaHoldAccept";

    public static final String COMMAND_VOJXTA_RESUME_REQUEST = "VoJxtaResumeRequest";

    public static final String COMMAND_VOJXTA_RESUME_ACCEPT = "VoJxtaResumeAccept";

    public static final String COMMAND_VOJXTA_HANGUP_REQUEST = "VoJxtaHangUpRequest";

    public static final String COMMAND_VOJXTA_HANGUP_ACCEPT = "VoJxtaHangUpAccept";

    public static final String COMMAND_VOJXTA_DISCONNECT_REQUEST = "VoJxtaDisconnectRequest";

    public static final String COMMAND_VOJXTA_DISCONNECT_ACCEPT = "VoJxtaDisconnectAccept";


    /**
     * internal session state
     */


    public static final int SESSION_VOJXTA_DISCONNECTED = 10;

    public static final int SESSION_VOJXTA_DISCONNECTING = 11;

    public static final int SESSION_VOJXTA_CONNECTED = 12;

    public static final int SESSION_VOJXTA_CONNECTING = 13;

    public static final int SESSION_VOJXTA_STARTING = 20;

    public static final int SESSION_VOJXTA_STARTED = 30;

    public static final int SESSION_VOJXTA_ENDING = 40;

    public static final int SESSION_VOJXTA_ENDED = 50;

    public static final int SESSION_VOJXTA_INCALL = 100;

    public static final int SESSION_VOJXTA_HOLDING = 110;

    public static final int SESSION_VOJXTA_DISCONNECT_REQUEST_SENT = 210;

    public static final int SESSION_VOJXTA_DISCONNECT_REQUEST_RECEIVED = 220;

    public static final int SESSION_VOJXTA_DISCONNECT_ACCEPT_SENT = 230;

    public static final int SESSION_VOJXTA_DISCONNECT_ACCEPT_RECEIVED = 240;

    public static final int SESSION_VOJXTA_RESUME_REQUEST_SENT = 250;

    public static final int SESSION_VOJXTA_RESUME_REQUEST_RECEIVED = 260;

    public static final int SESSION_VOJXTA_RESUME_ACCEPT_SENT = 270;

    public static final int SESSION_VOJXTA_RESUME_ACCEPT_RECEIVED = 280;

    public static final int SESSION_VOJXTA_HOLD_ACCEPT_SENT = 290;

    public static final int SESSION_VOJXTA_HOLD_ACCEPT_RECEIVED = 310;

    public static final int SESSION_VOJXTA_HOLD_REQUEST_SENT = 320;

    public static final int SESSION_VOJXTA_HOLD_REQUEST_RECEIVED = 330;

    public static final int SESSION_VOJXTA_START_REQUEST_SENT = 340;

    public static final int SESSION_VOJXTA_START_REQUEST_RECEIVED = 350;

    public static final int SESSION_VOJXTA_START_ACCEPT_SENT = 360;

    public static final int SESSION_VOJXTA_START_ACCEPT_RECEIVED = 370;

    public static final int SESSION_VOJXTA_HANGUP_ACCEPT_RECEIVED = 380;

    public static final int SESSION_VOJXTA_HANGUP_ACCEPT_SENT = 390;

    public static final int SESSION_VOJXTA_HANGUP_REQUEST_RECEIVED = 410;

    public static final int SESSION_VOJXTA_HANGUP_REQUEST_SENT = 420;

    public static final int SESSION_VOJXTA_INVITE_REQUEST_SENT = 430;

    public static final int SESSION_VOJXTA_INVITE_REQUEST_RECEIVED = 440;

    public static final int SESSION_VOJXTA_INVITE_ACCEPT_SENT = 450;

    public static final int SESSION_VOJXTA_INVITE_ACCEPT_RECEIVED = 460;

    public int sessionState = SESSION_VOJXTA_DISCONNECTED;

    public int protocolState = SESSION_VOJXTA_DISCONNECTED;

    public static final int MINIMUM_QUALITY = 0;

    public static final int MAXIMUM_QUALITY = 10;

    public static final int DEFAULT_QUALITY = 1;

    public static final long SECOND = 1000;

    public static final long MINUTE = SECOND * 60;

    public static final long HOUR = MINUTE * 60;

    public static final String TIME_SEPARATOR = ":";

    private int localVoiceQuality = DEFAULT_QUALITY;

    private int remoteVoiceQuality = DEFAULT_QUALITY;

    private long messageAckTimeout = DEFAULT_MESSAGE_ACK_TIMEOUT;

    private Dialog vojxtaDialog = null;

    private View view = null;

    private MyJXTA controller = null;

    private DialogMessage templateMessage = null;

    private VoJxtaDialogView vojxtaView = null;

    private static final int CONNECT_SLEEP_TIME = 200;

    private boolean sessionPersisted = false;

    private String sessionFileName = null;

    static final Logger LOG = Logger.getLogger(VoJxtaCallControl.class.getName());

    private long callStartTime = 0;

    private long callEndTime = 0;

    private long callElapsedTime = 0;

    /**
     * audio device OUT
     */
    private VoiceSpeakerOutput speakerControl = null;

    /**
     * audio device IN
     */
    private VoiceMicrophoneInput micControl = null;

    private final boolean locallyInitiated = false;

    private ReceivedMessageTimeoutThread receiveTimeoutThread = null;

    /**
     * started on command message dispatch. waits for ack to message (notofy)
     * then dies
     */
    private MessageAckThread messageAckThread = null;

    public static final Hashtable qualityByteMap = new Hashtable();
    /**  This maps quality level to encoded message byte size gleaned from
     *  speex. Do it this way allows us to dynamically change quality in a session
     *  as well as alowing the user to select quality at session start
     */

    static {
        qualityByteMap.put(new Integer(0), new Integer(10));
        qualityByteMap.put(new Integer(1), new Integer(15));
        qualityByteMap.put(new Integer(2), new Integer(20));
        qualityByteMap.put(new Integer(3), new Integer(25));
        qualityByteMap.put(new Integer(4), new Integer(32));
        qualityByteMap.put(new Integer(5), new Integer(42));
        qualityByteMap.put(new Integer(6), new Integer(52));
        qualityByteMap.put(new Integer(7), new Integer(60));
        qualityByteMap.put(new Integer(8), new Integer(70));
        qualityByteMap.put(new Integer(9), new Integer(86));
        qualityByteMap.put(new Integer(10), new Integer(106));

    }

    /**
     * Creates a new instance of VojxtaCallControl
     */
    public VoJxtaCallControl(VoJxtaDialogView vojxtaView, View p_view, Dialog dialog, boolean locallyInitiated) {

        LOG.setLevel(Level.INFO);


        this.vojxtaView = vojxtaView;

        this.vojxtaDialog = dialog;

        // wait for pipes to connect
        if (this.vojxtaDialog != null) {
            new Thread(new Runnable() {

                public void run() {
                    while (!VoJxtaCallControl.this.vojxtaDialog.isConnected()) {
                        try {
                            Thread.sleep(CONNECT_SLEEP_TIME);
                            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                                LOG.info("Waiting for is connected ");
                            }
                        }
                        catch (InterruptedException ignored) {
                        }
                    }
                }
            }, getClass().getName() + ":isConnected").start();
        }

        this.view = p_view;

        this.controller = p_view.getControl();

        if (!this.vojxtaDialog.isConnected()) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("Constructor : VoJxtaDialog NOT connected!! Resources released. Module Shutdown.");
            }

        } else {
            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("Constructor : VoJxtaDialog Connected!! Obtaining Resources. Starting Module");
            }

            this.speakerControl = new VoiceSpeakerOutput(this);

            this.micControl = new VoiceMicrophoneInput(this);


            templateMessage = new DialogMessage(this.vojxtaDialog.getGroup()
                    .getPeerGroup().getPeerName(), null, this.vojxtaDialog.getGroup()
                    .getPeerGroup().getPeerGroupID().toString(), this.vojxtaDialog
                    .getGroup().getPeerGroup().getPeerGroupName());

        }

    }//constructor

    /**
     * A templete DialogMessage is already constructed. All we need do is call
     * this method and clone. This will cleanup the code for message production
     * as well offer the possibility of a speedup in message creation.
     *
     * @return templeted DialogMessage
     */
    public DialogMessage getTemplateMessage() {
        return this.templateMessage;
    }

    public String getOriginator() {

        return speakerControl.getOriginator();
    }

    /** The following methods are offered as an interface to the UI for any
     *  statistics we can gather.
     */

    /**
     * Returns the system time in ms this call was started. Only valid after
     * call ha started
     */
    public long getCallStartTime() {

        return callStartTime;
    }

    /**
     * Returns a formated string of the call start time
     */
    public String getCallStartTimeString() {

        return new Date(callStartTime).toString();
    }

    /**
     * Returns the system time is ms this call ended. Is only valid after call
     * has ended.
     */
    public long getCallEndTime() {

        return callEndTime;
    }

    /**
     * Returns a formated string of hte call end time.
     */
    public String getCallEndTimeString() {

        return ((callEndTime != 0) ? new Date(callEndTime).toString() : "");
    }

    /**
     * Returns the time elapsed in ms from call start time. Only valid after
     * call has started
     */
    public long getCallElapsedTime() {

        return callElapsedTime = System.currentTimeMillis() - getCallStartTime();
    }

    /**
     * Returns formated string of the call elapsed time
     */
    public String getCallElapseTimeString() {

        return formatElapsedTime(getCallElapsedTime());
    }

    /**
     * Returns the size of outgoing messages. Outgoing messages are those that
     * have been encoded and are ready for delivery
     */
    public int getEncodeMessageSize() {

        return micControl.getEncodedMessageSize();
    }

    /**
     * Returns the size of the FIFO byte buffer holding encoded messages that
     * ready for delivery.
     */
    public int getEncodeBufferSize() {

        return micControl.getEncodedBufferSize();
    }

    /**
     * Returns the quality of voice encoding performed on audio in blocks.
     */
    public int getQualityOfService() {

        return micControl.getEncodeQuality();
    }

    /**
     * Returns the ratio as int of raw audio in block  to  encoded audio blocks
     */
    public int getCompressionRatio() {

        int blockSize = micControl.getEncodedBlockSize();

        if (blockSize == 0) {
            blockSize = 1;
        }

        return micControl.getAudioBlockSize() / blockSize;
    }


    /**
     * Returns the average time it takes to encode raw bytes from audio in.
     */
    public int getAverageEncodeTime() {

        return micControl.getAverageEncodeTime();
    }

    /**
     * Returns the average time it takes to decode encoded bytes from pipe in
     */
    public long getAverageDecodeTime() {

        return speakerControl.getAverageDecodeTime();
    }

    /**
     * Returns a string formated to represent hours:minutes:seconds of the time
     * elapsed since call start.
     */
    protected String formatElapsedTime(long elapsedTime) {

        long tmp = elapsedTime;

        long hours = tmp / HOUR;

        tmp = tmp - (hours * HOUR);

        long minutes = tmp / MINUTE;

        tmp = tmp - (minutes * MINUTE);

        long seconds = tmp / SECOND;

        String timeString = null;

        if (hours < 10) {
            timeString = "0";
        }
        timeString = String.valueOf(hours) + this.TIME_SEPARATOR;
        ;

        if (minutes < 10) {
            timeString = timeString + "0";
        }
        timeString = timeString + String.valueOf(minutes) + this.TIME_SEPARATOR;

        if (seconds < 10) {
            timeString = timeString + "0";
        }
        timeString = timeString + String.valueOf(seconds);

        return timeString;
    }

    /**
     * Sets the size of the outgoing buffer holding encoded audio blocks. This
     * Should be adjusted along with the encoded message size or a buffer
     * overflow could occur.
     */
    public void setOutgoingEncodedBufferSize(int bufferSize) {

        micControl.setEncodedBufferSize(bufferSize);

    }

    /**
     * Sets the size of the incoming buffer holding encoded audio blocks. This
     * should be adjusted along with the encoded message size or a buffer
     * overflow could occur.
     */
    public void setIncomingEncodedBufferSize(int bufferSize) {

        speakerControl.setEncodedBufferSize(bufferSize);
    }

    /**
     * Returns the size of the outgoing buffer holding the encoded audio
     * blocks.
     */
    public int getOutgoingEncodedBufferSize() {

        return micControl.getEncodedBufferSize();
    }

    /**
     * Returns the size of the incoming buffer holding the encoded audio
     * blocks
     */
    public int getIncomingEncodedBufferSize() {

        return speakerControl.getEncodedBufferSize();
    }

    /**
     * Sets the size of the block sent to source data line (speaker). This
     * value can affect latency of audio messages. Possibly create choppyness
     */
    public void setIncomingEncodedMessageSize(int messageSize) {

        //speakerControl.setEncodedMessageSize (messageSize);
    }

    /**
     * Sets the number of encoded bytes per message sent to the remote peer on
     * this pipe
     */
    public void setOutgoingEncodedMessageSize(int messageSize) {

        //micControl.setEncodedMessageSize (messageSize);
    }

    /**
     * Returns the number of bytes taken from the buffer to be player on the
     * source data line (speaker)
     */
    public int getIncomingEncodedMessageSize() {

        return speakerControl.getEncodedMessageSize();
    }

    /**
     * Returns the number of bytes of encoded audio data per message sent on
     * this pipe.
     */
    public int getOutgoingEncodedMessageSize() {

        return micControl.getEncodedMessageSize();
    }

    /**
     * Returns the size in bytes of raw audio read from target line (mic)
     */
    public int getAudioBlockSize() {

        return micControl.getAudioBlockSize();
    }

    /**
     * Returns the basic block size in bytes of encoded audio.
     */
    public int getEncodedBlockSize() {

        return micControl.getEncodedBlockSize();
    }

    /**
     * Returns a string representing the name of the remote peer
     */
    public String getRemoteParticipant() {

        return new String("Remote");
    }

    /**
     * Returns the Dialog that this module is attched.
     */
    public Dialog getDialog() {

        return this.vojxtaDialog;
    }

    /**
     * Called on session end.  Closes all open audio resources.
     */
    public void destroy() {

        if (micControl != null) {

            micControl.endMic();

            micControl.releaseHardware();
        }

        micControl = null;

        if (speakerControl != null) {


            speakerControl.endSpeaker();

            speakerControl.releaseHardware();
        }

        speakerControl = null;


    }


    /**
     * Returns the audio out control.
     */
    public VoiceSpeakerOutput getSpeakerControl() {

        return this.speakerControl;
    }

    /**
     * Returns the audio input control.
     */
    public VoiceMicrophoneInput getMicControl() {

        return this.micControl;
    }


    protected void initAudioSystem() {


        micControl.obtainHardware();

        speakerControl.obtainHardware();
    }

    /**
     * Executes the session protocol logic.
     * <p/>
     * basic order of operations:
     * SessionInviteRequest -->
     * <-- SessionInviteAccept
     * CallStartRequest -->
     * <-- CallStartAccept
     * CallHangupRequest -->
     * <-- CallHangupAccept
     * SessionEndRequest -->
     * <-- SessionEndAccept
     * <p/>
     * The Peer to Send an invitation controls the session start and call start.
     * After this either peer may place the call on hold. Once on hold only the
     * peer to place the call on hold may resume the call. CallHangup may be
     * performed by either peer. Quality elements are added to startcall
     * commands. Voice data messages are not squenced and do not require ack's.
     * Session logic is limited to two peers. Protocol state is used as an
     * event notification mechanism and to determine protocol command order.
     * This is not an adaptive protocol. Strict order of operations is observed
     */
    public void callControl(String command, DialogMessage msg) {
        LOG.setLevel(Level.INFO);

        if (COMMAND_VOJXTA_INVITE_REQUEST.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " + this.COMMAND_VOJXTA_INVITE_REQUEST +
                        " received");
            }


            if (getProtocolState() == this.SESSION_VOJXTA_DISCONNECTED) {

                setProtocolState(this.SESSION_VOJXTA_INVITE_REQUEST_RECEIVED);

            } else {
                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSION_DISCONNECTED - got " + command);
                }
            }

        } else if (COMMAND_VOJXTA_INVITE_ACCEPT.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " + this.COMMAND_VOJXTA_INVITE_ACCEPT +
                        " received");
            }


            if (getProtocolState() == this.SESSION_VOJXTA_INVITE_REQUEST_SENT) {

                updateAckThread(command);

                setProtocolState(this.SESSION_VOJXTA_INVITE_ACCEPT_RECEIVED);

                localActionSendStartRequest();

            } else {
                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSION_VOJXTA_INVITE_REQUEST_SENT - got " +
                            command);
                }
            }

        } else if (COMMAND_VOJXTA_START_REQUEST.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " + this.COMMAND_VOJXTA_START_REQUEST +
                        " received");
            }

            initAudioSystem();

            if (getProtocolState() == this.SESSION_VOJXTA_INVITE_ACCEPT_SENT) {

                setProtocolState(this.SESSION_VOJXTA_START_REQUEST_RECEIVED);

                localActionStartRequestReceived(msg);

                localActionStartVoiceTransmision();

                setProtocolState(this.SESSION_VOJXTA_INCALL);

            } else {
                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSIONVOJXTA_INVITE_ACCEPT_SENT - got " +
                            command);
                }
            }

        } else if (COMMAND_VOJXTA_START_ACCEPT.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " + this.COMMAND_VOJXTA_START_ACCEPT +
                        " received");
            }

            initAudioSystem();

            if (getProtocolState() == this.SESSION_VOJXTA_START_REQUEST_SENT) {

                updateAckThread(command);

                setProtocolState(this.SESSION_VOJXTA_START_ACCEPT_RECEIVED);

                localActionStartAcceptReceived(msg);
                // this is the last command received before starting voice
                // transmission/reception. signal thread to start sending vojxtadata

                localActionStartVoiceTransmision();

                setProtocolState(this.SESSION_VOJXTA_INCALL);

            } else {
                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSION_VOJXTA_START_REQUEST_SENT - got " +
                            command);
                }
            }

        } else if (COMMAND_VOJXTA_HOLD_REQUEST.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " + this.COMMAND_VOJXTA_HOLD_REQUEST +
                        " received");
            }


            if (getProtocolState() == this.SESSION_VOJXTA_INCALL) {


                setProtocolState(this.SESSION_VOJXTA_HOLD_REQUEST_RECEIVED);

                sendCommand(this.COMMAND_VOJXTA_HOLD_ACCEPT, null);

                setProtocolState(this.SESSION_VOJXTA_HOLD_ACCEPT_SENT);

                //place call on hold - we did not initiate the hold

                localActionHoldCall(false);

                setProtocolState(this.SESSION_VOJXTA_HOLDING);

            } else {
                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSION_VOJXTA_INCALL - got " + command);
                }
            }

        } else if (COMMAND_VOJXTA_HOLD_ACCEPT.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " + this.COMMAND_VOJXTA_HOLD_ACCEPT +
                        " received");
            }

            if (getProtocolState() == this.SESSION_VOJXTA_HOLD_REQUEST_SENT) {

                updateAckThread(command);

                setProtocolState(this.SESSION_VOJXTA_HOLD_ACCEPT_RECEIVED);
                //place call on hold - we initiate the hold
                localActionHoldCall(true);

                setProtocolState(this.SESSION_VOJXTA_HOLDING);

            } else {
                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSION_VOJXTA_HOLD_REQUEST_SENT - got " +
                            command);
                }
            }

        } else if (COMMAND_VOJXTA_RESUME_REQUEST.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " + this.COMMAND_VOJXTA_RESUME_REQUEST +
                        " received");
            }

            if (getProtocolState() == this.SESSION_VOJXTA_HOLDING) {

                setProtocolState(this.SESSION_VOJXTA_RESUME_REQUEST_RECEIVED);

                sendCommand(this.COMMAND_VOJXTA_RESUME_ACCEPT, null);

                setProtocolState(this.SESSION_VOJXTA_RESUME_ACCEPT_SENT);

                // resume call
                localActionResumeCall();

                setProtocolState(this.SESSION_VOJXTA_INCALL);

            } else {
                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSION_VOJXTA_HOLDING - got " + command);
                }
            }

        } else if (COMMAND_VOJXTA_RESUME_ACCEPT.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " + this.COMMAND_VOJXTA_RESUME_ACCEPT +
                        " received");
            }

            if (getProtocolState() == this.SESSION_VOJXTA_RESUME_REQUEST_SENT) {

                updateAckThread(command);

                setProtocolState(this.SESSION_VOJXTA_RESUME_ACCEPT_RECEIVED);

                // resume call
                localActionResumeCall();

                setProtocolState(this.SESSION_VOJXTA_INCALL);

            } else {
                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSION_VOJXTA_RESUME_REQUEST_SENT - got " +
                            command);
                }
            }

        } else if (COMMAND_VOJXTA_HANGUP_REQUEST.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " + this.COMMAND_VOJXTA_HANGUP_REQUEST +
                        " received");
            }


            if (getProtocolState() == this.SESSION_VOJXTA_INCALL) {

                setProtocolState(this.SESSION_VOJXTA_HANGUP_REQUEST_RECEIVED);

                sendCommand(this.COMMAND_VOJXTA_HANGUP_ACCEPT, null);

                setProtocolState(this.SESSION_VOJXTA_HANGUP_ACCEPT_SENT);

                // hangup call - stop transfering data
                localActionEndCall();

                setProtocolState(this.SESSION_VOJXTA_ENDED);

            } else {
                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSION_VOJXTA_INCALL - got " + command);
                }
            }

        } else if (COMMAND_VOJXTA_HANGUP_ACCEPT.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " + this.COMMAND_VOJXTA_HANGUP_ACCEPT +
                        " received");
            }


            if (getProtocolState() == this.SESSION_VOJXTA_HANGUP_REQUEST_SENT) {

                updateAckThread(command);

                setProtocolState(this.SESSION_VOJXTA_HANGUP_ACCEPT_RECEIVED);

                //hang up call
                localActionEndCall();

                sendCommand(this.COMMAND_VOJXTA_DISCONNECT_REQUEST, this.COMMAND_VOJXTA_DISCONNECT_ACCEPT);

                setProtocolState(this.SESSION_VOJXTA_ENDED);


            } else {
                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSION_VOJXTA_HANGUP_REQUEST_SENT - got " +
                            command);
                }
            }

        } else if (COMMAND_VOJXTA_DISCONNECT_REQUEST.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " +
                        this.COMMAND_VOJXTA_DISCONNECT_REQUEST + " received");
            }

            if (getProtocolState() == this.SESSION_VOJXTA_ENDED) {

                setProtocolState(this.SESSION_VOJXTA_DISCONNECT_REQUEST_RECEIVED);

                sendCommand(this.COMMAND_VOJXTA_DISCONNECT_ACCEPT, null);

                setProtocolState(this.SESSION_VOJXTA_DISCONNECT_ACCEPT_SENT);

                //disconnect from host - close window no more calls from this tab

                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("Disconnecting");

                }

                localActionEndSession();

                setProtocolState(this.SESSION_VOJXTA_DISCONNECTED);

            } else {
                // the other side is going down... kill session
                // this is the non graceful shutdown withthe protocol being in any state
                setProtocolState(this.SESSION_VOJXTA_DISCONNECT_REQUEST_RECEIVED);

                sendCommand(this.COMMAND_VOJXTA_DISCONNECT_ACCEPT, null);

                setProtocolState(this.SESSION_VOJXTA_DISCONNECT_ACCEPT_SENT);

                //disconnect from host - close window no more calls from this tab
                localActionEndSession();

                setProtocolState(this.SESSION_VOJXTA_DISCONNECTED);

                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSION_VOJXTA_ENDED - got " + command);
                    LOG.info("Ungraceful disconnect received...Going Down");
                }
            }

        } else if (COMMAND_VOJXTA_DISCONNECT_ACCEPT.equals(command)) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("callControl : " +
                        this.COMMAND_VOJXTA_DISCONNECT_ACCEPT + " received");
            }

            if (getProtocolState() == this.SESSION_VOJXTA_ENDED) {

                updateAckThread(command);

                setProtocolState(this.SESSION_VOJXTA_DISCONNECT_ACCEPT_RECEIVED);

                //disconnect from host - close window no more calls from this tab
                localActionEndSession();

                setProtocolState(this.SESSION_VOJXTA_DISCONNECTED);

            } else {
                // the other side is going down... kill session
                // TODO:

                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("expected SESSION_VOJXTA_ENDED - got " + command);
                }
            }
        }

    }//call control


    /**
     * message has been received... determine if its the correct response
     */
    protected void updateAckThread(String command) {
        LOG.info("Checking AckThread");

        if (messageAckThread != null) {

            messageAckThread.setMessageAcknowledged(command);

            if (messageAckThread.isThreadEnded()) {

                messageAckThread = null;
            }
        } else {
            LOG.info("MessageAckThread is null!!");
        }
    }

    /**
     * these methods offer an interface to audio controls. volume, mute etc
     */

    public boolean isMicGainSupported() {

        return micControl.isGainControlSupported();
    }

    public float getMicGainValue() {

        return micControl.getGainValue();
    }

    /**
     * Control state accessor
     */
    public void adjustMicGainControl(float newValue) {

        micControl.adjustGainControl(newValue);
    }

    /**
     * Control state accessor
     */
    public float getMicMaxGain() {

        return micControl.getMaxGainValue();
    }

    /**
     * Control state accessor
     */
    public float getMicMinGain() {

        return micControl.getMinGainValue();
    }

    /**
     * Control state accessor
     */
    public String getMicGainUnits() {

        return micControl.getGainUnits();
    }

    /**
     * Control state accessor
     */
    public String getMicMaxGainLabel() {

        return micControl.getMaxLabel();
    }

    /**
     * Control state accessor
     */
    public String getMicMinGainLabel() {

        return micControl.getMinLabel();
    }

    /**
     * Control state accessor
     */
    public boolean isSpeakerGainSupported() {

        return speakerControl.isGainControlSupported();
    }

    /**
     * Control state accessor
     */
    public float getSpeakerGainValue() {

        return speakerControl.getGainValue();
    }

    /**
     * Control state accessor
     */
    public void adjustSpeakerGainControl(float newValue) {

        speakerControl.adjustGainValue(newValue);
    }

    /**
     * Control state accessor
     */
    public float getSpeakerMaxGain() {

        return speakerControl.getMaxGainValue();
    }

    /**
     * Control state accessor
     */
    public float getSpeakerMinGain() {

        return speakerControl.getMinGainValue();
    }

    /**
     * Control state accessor
     */
    public String getSpeakerGainUnits() {

        return speakerControl.getGainUnits();
    }

    /**
     * Control state accessor
     */
    public String getSpeakerMaxGainLabel() {

        return speakerControl.getMaxGainLabel();
    }

    /**
     * Control state accessor
     */
    public String getSpeakerMinGainLabel() {

        return speakerControl.getMinGainLabel();
    }


    /**
     * Returns the voice quality set by local user
     */
    protected int getLocalVoiceQuality() {

        return localVoiceQuality;
    }

    /**
     * Called by the UI to set prefered voice quality
     */
    public void setLocalVoiceQuality(int localVoiceQuality) {

        if (localVoiceQuality >= this.MINIMUM_QUALITY && localVoiceQuality <= this.MAXIMUM_QUALITY) {

            this.localVoiceQuality = localVoiceQuality;

        } else {

            this.localVoiceQuality = this.DEFAULT_QUALITY;
        }


    }

    /**
     * Returns the voice quality preferred by thte remote peer
     */
    protected int getRemoteVoiceQuality() {

        return remoteVoiceQuality;

    }

    /**
     * Sets the preferred voice quality of the remote peer
     */
    protected void setRemoteVoiceQuality(int remoteVoiceQuality) {

        if (remoteVoiceQuality >= this.MINIMUM_QUALITY && remoteVoiceQuality <= this.MAXIMUM_QUALITY) {

            this.remoteVoiceQuality = remoteVoiceQuality;

        } else {

            this.remoteVoiceQuality = this.DEFAULT_QUALITY;
        }

    }

    /**
     * Sets the preferred voice quality of the remote peer
     */
    protected void setRemoteVoiceQuality(String remoteVoiceQuality) {

        try {

            setRemoteVoiceQuality(Integer.valueOf(remoteVoiceQuality).intValue());

        } catch (NumberFormatException nfx) {

            nfx.printStackTrace();

            setRemoteVoiceQuality(this.DEFAULT_QUALITY);

        }
    }

    /**
     * Sets preferred local voice quality.
     */
    public void setLocalVoiceQuality(String localVoiceQuality) {

        try {

            setLocalVoiceQuality(Integer.valueOf(localVoiceQuality).intValue());

        } catch (NumberFormatException nfx) {

            nfx.printStackTrace();

            setLocalVoiceQuality(this.DEFAULT_QUALITY);
        }
    }


    /**
     * Returns the lesser of local and remote preferred voice qualities.
     */
    public int getMinimumVoiceQuality() {

        int localVQ = getLocalVoiceQuality();

        int remoteVQ = getRemoteVoiceQuality();

        int rtn = 0;

        if (localVQ == remoteVQ) {

            rtn = localVQ;

        } else if (localVQ < remoteVQ) {

            rtn = localVQ;

        } else {

            rtn = remoteVQ;
        }

        return rtn;
    }

    /**
     * Statistical accessor
     */
    public int getIncomingBufferSize() {

        return speakerControl.getBufferSize();
    }

    /**
     * Statistical accessor
     */
    public int getIncomingBufferCapacity() {

        return speakerControl.getBufferCapacity();
    }

    /**
     * Statistical accessor
     */
    public int getOutgoingBufferSize() {

        return micControl.getBufferSize();
    }

    /**
     * Statistical accessor
     */
    public int getOutgoingBufferCapacity() {

        return micControl.getBufferCapacity();
    }


    /**
     * Command StartAccept received from remote
     */
    protected void localActionStartAcceptReceived(DialogMessage msg) {


        MessageElement me = msg.getMessageElement(this.TAG_QUALITY_ELEMENT);

        if (me != null) {

            ByteArrayMessageElement remoteVoiceQualityElement = (ByteArrayMessageElement) me;

            String remoteVoiceQualityString = remoteVoiceQualityElement.toString();

            if (remoteVoiceQualityString != null) {

                setRemoteVoiceQuality(remoteVoiceQualityString);

            }
        }
    }


    /**
     * Command SendStartRquest to remote.
     */
    protected void localActionSendStartRequest() {

        /*
        DialogMessage startRequestMsg = (DialogMessage)templateMessage.clone ();

       StringMessageElement commandElement = new StringMessageElement (
               TAG_SESSION_COMMAND, this.COMMAND_VOJXTA_START_REQUEST, null);

       startRequestMsg.addMessageElement (TAG_SESSION_COMMAND, commandElement);

       String voiceQualityString = String.valueOf (getLocalVoiceQuality ());

       StringMessageElement voiceQualityElement = new StringMessageElement (
               TAG_QUALITY_ELEMENT, voiceQualityString, null);

       startRequestMsg.addMessageElement (TAG_QUALITY_ELEMENT, voiceQualityElement);

       this.vojxtaDialog.dispatch (startRequestMsg);
        */

        String voiceQualityString = String.valueOf(getLocalVoiceQuality());

        StringMessageElement voiceQualityElement = new StringMessageElement(
                TAG_QUALITY_ELEMENT, voiceQualityString, null);

        HashMap elementMap = new HashMap();

        elementMap.put(TAG_QUALITY_ELEMENT, voiceQualityElement);

        sendCommand(this.COMMAND_VOJXTA_START_REQUEST,
                this.COMMAND_VOJXTA_START_ACCEPT,
                elementMap);

        setProtocolState(this.SESSION_VOJXTA_START_REQUEST_SENT);

    }


    /**
     * Command StartRquest received from remote
     */
    protected void localActionStartRequestReceived(DialogMessage msg) {

        /** retrieve the quality element */
        MessageElement me = msg.getMessageElement(this.TAG_QUALITY_ELEMENT);

        if (me != null) {


            ByteArrayMessageElement remoteVoiceQualityElement = (ByteArrayMessageElement) me;

            String remoteVoiceQualityString = remoteVoiceQualityElement.toString();

            if (remoteVoiceQualityString != null) {

                setRemoteVoiceQuality(remoteVoiceQualityString);

            }

            /** determine who has the lowest quality and send to remote */

            String commonVoiceQualityString = String.valueOf(getMinimumVoiceQuality());
            /*
            DialogMessage startAcceptMsg = (DialogMessage)templateMessage.clone ();
             
            StringMessageElement commandElement = new StringMessageElement (
                    TAG_SESSION_COMMAND, this.COMMAND_VOJXTA_START_ACCEPT, null);
             
            startAcceptMsg.addMessageElement (TAG_SESSION_COMMAND, commandElement);
             
            StringMessageElement voiceQualityElement = new StringMessageElement (
                    TAG_QUALITY_ELEMENT, commonVoiceQualityString, null);
             
            startAcceptMsg.addMessageElement (TAG_QUALITY_ELEMENT, voiceQualityElement);
             
            this.vojxtaDialog.dispatch (startAcceptMsg);
             */
            StringMessageElement voiceQualityElement = new StringMessageElement(
                    TAG_QUALITY_ELEMENT, commonVoiceQualityString, null);

            HashMap elementMap = new HashMap();

            elementMap.put(TAG_QUALITY_ELEMENT, voiceQualityElement);

            sendCommand(this.COMMAND_VOJXTA_START_ACCEPT, null, elementMap);

            setProtocolState(this.SESSION_VOJXTA_START_ACCEPT_SENT);

        } else {
            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("remote voice quality element is NULL");
            }
        }
    }

    /**
     * Sets if this sesseion is to be persisted
     * TODO
     */
    public void setPersistSession(boolean persist, String fileName) {

        this.sessionPersisted = persist;

        this.sessionFileName = fileName;

    }

    /**
     * determines if this audio session is to be persisted
     */
    public boolean isSessionPersisted() {

        return this.sessionPersisted;
    }

    /**
     * Return the file name to which we persist this audio session
     */
    public String getSessionPersistFileName() {

        return this.sessionFileName;
    }

    /**
     * Called by UI to alert us to start this audio call
     */
    public void viewActionPlaceCall() {

        sendCommand(this.COMMAND_VOJXTA_INVITE_REQUEST, this.COMMAND_VOJXTA_INVITE_ACCEPT);

        setProtocolState(this.SESSION_VOJXTA_INVITE_REQUEST_SENT);
    }


    /**
     * Command EndCall reveived from remote peer
     */
    private void localActionEndCall() {

        if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
            LOG.info("localActionEndCall");
        }

        setCallEndTime();

        receiveTimeoutThread.stopThread();

        if (micControl != null) {

            micControl.endMic();

            micControl.releaseHardware();
        }

        if (speakerControl != null) {

            speakerControl.endSpeaker();

            speakerControl.releaseHardware();
        }


    }


    /**
     * start the audio controls for sending and receiving
     */
    private void localActionStartVoiceTransmision() {

        if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
            LOG.info("localActionStartVoiceTransmision");
        }

        setCallStartTime();

        micControl.beginMic();

        speakerControl.beginSpeaker();

        receiveTimeoutThread = new ReceivedMessageTimeoutThread();

        receiveTimeoutThread.start();

    }

    /**
     * Sets the time in ms this call started
     */
    protected void setCallStartTime() {

        this.callStartTime = System.currentTimeMillis();
    }

    /**
     * Sets the time in ms this call ended
     */
    protected void setCallEndTime() {

        this.callEndTime = System.currentTimeMillis();
    }

    /**
     * Command HoldCall reveived from remote
     */
    private void localActionHoldCall(boolean myHold) {

        micControl.pauseMic();

        speakerControl.pauseSpeaker();

    }

    /**
     *  Command Resume call received from remote
     */

    /**
     * Command ResumeCall received from remote
     */
    private void localActionResumeCall() {

        micControl.resumeMic();

        speakerControl.resumeSpeaker();
    }

    /**
     * Command EndSession received from remote
     */
    private void localActionEndSession() {

        if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
            LOG.info("localActionEndSession");
        }

        if (receiveTimeoutThread != null) {

            receiveTimeoutThread.stopThread();
        }

        if (micControl != null) {

            micControl.endMic();

            micControl.releaseHardware();
        }

        if (speakerControl != null) {

            speakerControl.endSpeaker();

            speakerControl.releaseHardware();
        }

    }


    /**
     * Called from UI signaling user accepts call invitation
     */
    public void viewActionAcceptCall() {

        if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
            LOG.info("viewActionAcceptCall");
        }

        sendCommand(this.COMMAND_VOJXTA_INVITE_ACCEPT, null);

        setProtocolState(this.SESSION_VOJXTA_INVITE_ACCEPT_SENT);

    }

    /**
     * Called from IU signaling user wishes to place call on hold.
     */
    public void viewActionHoldCall() {

        if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
            LOG.info("viewActionHoldCall");
        }

        sendCommand(this.COMMAND_VOJXTA_HOLD_REQUEST, this.COMMAND_VOJXTA_HOLD_ACCEPT);

        setProtocolState(this.SESSION_VOJXTA_HOLD_REQUEST_SENT);

    }

    /**
     * Called from IU signaling user wishes to hangup call.
     */
    public void viewActionHangupCall() {

        if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
            LOG.info("viewActionHangupCall");
        }

        sendCommand(this.COMMAND_VOJXTA_HANGUP_REQUEST, this.COMMAND_VOJXTA_HANGUP_ACCEPT);

        setProtocolState(this.SESSION_VOJXTA_HANGUP_REQUEST_SENT);

        setCallEndTime();

        micControl.endMic();

        micControl.releaseHardware();

        speakerControl.endSpeaker();

        speakerControl.releaseHardware();
    }

    /**
     * Called from IU signaling user wishes to resume a call previsously placed
     * on hold.
     */
    public void viewActionResumeCall() {

        if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
            LOG.info("viewActionResumeCall");
        }

        sendCommand(this.COMMAND_VOJXTA_RESUME_REQUEST, this.COMMAND_VOJXTA_RESUME_ACCEPT);

        setProtocolState(this.SESSION_VOJXTA_RESUME_REQUEST_SENT);

    }

    private void sendCommand(String command, String ackCommand) {

        sendCommand(command, ackCommand, null);
    }

    /**
     * Sends a command on the vojxta dialog pipe. This does not change the
     * Protocol state. We have to remember to do this ourselves.
     */
    private void sendCommand(String command, String ackCommand, HashMap elementMap) {

        if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
            LOG.info("sendCommand : " + command);
        }

        DialogMessage msg = (DialogMessage) templateMessage.clone();

        StringMessageElement commandElement = new StringMessageElement(
                TAG_SESSION_COMMAND, command, null);

        msg.addMessageElement(TAG_SESSION_COMMAND, commandElement);

        if (elementMap != null) {

            Iterator keys = elementMap.keySet().iterator();

            while (keys.hasNext()) {

                String elementName = (String) keys.next();

                MessageElement element = (MessageElement) elementMap.get(elementName);

                msg.addMessageElement(elementName, element);
                LOG.info("added element " + elementName);
            }
        }

        if (ackCommand != null) {

            this.messageAckThread = new MessageAckThread(ackCommand);

            this.messageAckThread.start();
        }

        this.vojxtaDialog.dispatch(msg);


    }

    /**
     * Session state was meant for the UI as a messaging signaler to users
     */
    private void setSessionState(int sessionState) {

        this.sessionState = sessionState;

        this.vojxtaView.sessionStateChanged(this.sessionState);

    }

    /**
     * Returns the current session state
     */
    public int getSessionState() {

        return this.sessionState;

    }

    /**
     * This method updates the current protocol state. Also signals the UI
     * of a protocol state change.
     */
    private void setProtocolState(int protocolState) {

        this.protocolState = protocolState;

        if (this.protocolState == this.SESSION_VOJXTA_INVITE_REQUEST_SENT ||
                this.protocolState == this.SESSION_VOJXTA_INVITE_REQUEST_RECEIVED) {

            setSessionState(this.SESSION_VOJXTA_CONNECTING);


        } else if (this.protocolState == this.SESSION_VOJXTA_INVITE_ACCEPT_SENT ||
                this.protocolState == this.SESSION_VOJXTA_INVITE_ACCEPT_RECEIVED) {

            setSessionState(this.SESSION_VOJXTA_CONNECTED);

        } else if (this.protocolState == this.SESSION_VOJXTA_START_REQUEST_SENT ||
                this.protocolState == this.SESSION_VOJXTA_START_REQUEST_RECEIVED) {

            setSessionState(this.SESSION_VOJXTA_STARTING);

        } else if (this.protocolState == this.SESSION_VOJXTA_START_ACCEPT_SENT ||
                this.protocolState == this.SESSION_VOJXTA_START_ACCEPT_RECEIVED) {

            setSessionState(this.SESSION_VOJXTA_STARTED);

        } else if (this.protocolState == this.SESSION_VOJXTA_HOLD_ACCEPT_SENT ||
                this.protocolState == this.SESSION_VOJXTA_HOLD_ACCEPT_RECEIVED) {

            setSessionState(this.SESSION_VOJXTA_HOLDING);

        } else if (this.protocolState == this.SESSION_VOJXTA_INCALL) {

            setSessionState(this.SESSION_VOJXTA_INCALL);

        } else if (this.protocolState == this.SESSION_VOJXTA_RESUME_ACCEPT_SENT ||
                this.protocolState == this.SESSION_VOJXTA_RESUME_ACCEPT_RECEIVED) {

            setSessionState(this.SESSION_VOJXTA_INCALL);

        } else if (this.protocolState == this.SESSION_VOJXTA_HANGUP_REQUEST_SENT ||
                this.protocolState == this.SESSION_VOJXTA_HANGUP_REQUEST_RECEIVED) {

            setSessionState(this.SESSION_VOJXTA_ENDING);

        } else if (this.protocolState == this.SESSION_VOJXTA_HANGUP_ACCEPT_SENT ||
                this.protocolState == this.SESSION_VOJXTA_HANGUP_ACCEPT_RECEIVED) {

            setSessionState(this.SESSION_VOJXTA_ENDED);

        } else if (this.protocolState == this.SESSION_VOJXTA_DISCONNECT_REQUEST_SENT ||
                this.protocolState == this.SESSION_VOJXTA_DISCONNECT_REQUEST_RECEIVED) {

            setSessionState(this.SESSION_VOJXTA_DISCONNECTING);

        } else if (this.protocolState == this.SESSION_VOJXTA_DISCONNECT_ACCEPT_SENT ||
                this.protocolState == this.SESSION_VOJXTA_DISCONNECT_ACCEPT_RECEIVED) {

            setSessionState(this.SESSION_VOJXTA_DISCONNECTED);

        }
        if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
            LOG.info("ProtocolStateChanged to " + protocolState);
        }
        this.vojxtaView.protocolStateChanged(protocolState);

    }

    /**
     * Returns the protocol state
     */
    public int getProtocolState() {

        return this.protocolState;

    }


    /**
     * Returns the current time in milliseconds to wait for a message respose
     * before session shutdown.
     */
    public long getMessageAckTimeout() {

        return (messageAckTimeout > this.MINIMUM_MESSAGE_ACK_TIMEOUT) ?
                messageAckTimeout : DEFAULT_MESSAGE_ACK_TIMEOUT;
    }

    public void setMessageAckTimeout(long timeInMilliseconds) {

        if (timeInMilliseconds > MINIMUM_MESSAGE_ACK_TIMEOUT) {

            this.messageAckTimeout = timeInMilliseconds;
        }
    }

    /**
     * Message ack thread has timed out and no message ack has been received.
     * End session.
     */
    protected void localActionMessageGoneUnAcknowledged() {
        if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
            LOG.info(" localActionMessageGoneUnAcknowledged ");
        }
        localActionEndSession();

        /** Normally we'd just call sendcommand(END_SESSION) and wait for a
         *  end END_SEESION_ACCEPT then go down gracefully. Since we aren't
         *  receiving command messages we choose to just exit hard. */
        /*
         DialogMessage msg = (DialogMessage)templateMessage.clone ();
         
        StringMessageElement commandElement = new StringMessageElement (
                TAG_SESSION_COMMAND, this.COMMAND_VOJXTA_DISCONNECT_REQUEST, null);
         
        msg.addMessageElement (TAG_SESSION_COMMAND, commandElement);
         
        this.vojxtaDialog.dispatch (msg);
         **/
        sendCommand(this.COMMAND_VOJXTA_DISCONNECT_REQUEST, null);

        this.setProtocolState(this.SESSION_VOJXTA_DISCONNECTED);
    }


    /**
     * After a message is sent this thread is instantiated, started and will
     * wait till it's lock is notified of an incoming message or till a timeout
     * is reached. The idea being each commmand message dispatched will return
     * an accept response. If that response is received then +1 if not this thread
     * will notify us and shutdown the session as gracefully as possible. A new
     * Thread is started for each command message sent.
     * TODO
     * Better option then creating a new thread for each command. Reuse of a single
     * thread maybe.
     */
    class MessageAckThread extends Thread {

        private AckWaitLock ackWaitLock = null;

        private String commandName = null;

        private boolean threadEnded = false;

        public MessageAckThread(String commandName) {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("MessageAckThread waiting for command " + commandName);
            }

            ackWaitLock = new AckWaitLock();

            this.commandName = commandName;

            this.setPriority(Thread.MIN_PRIORITY);

            //this.setDaemon (true);
        }

        public void run() {

            if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                LOG.info("MessageAckThread : Run ");
            }

            try {

                synchronized (ackWaitLock) {

                    ackWaitLock.wait(getMessageAckTimeout());
                }
            } catch (InterruptedException ix) {

                ix.printStackTrace();
            }

            synchronized (ackWaitLock) {

                if (!ackWaitLock.isAcknowledged()) {
                    if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                        LOG.info("MessageAckThread : " + this.commandName +
                                " command NOT acknowledged");
                    }
                    VoJxtaCallControl.this.localActionMessageGoneUnAcknowledged();
                }
            }

            this.threadEnded = true;


        }

        /**
         * Avoids a race condition... needs to be reworked.
         */
        public boolean isThreadEnded() {

            while (!this.threadEnded) {

                try {
                    Thread.sleep(100);
                } catch (InterruptedException ix) {
                    ix.printStackTrace();
                }
            }

            return this.threadEnded;
        }

        /**
         * A message was received and this thread is alerted.
         */
        public void setMessageAcknowledged(String commandName) {

            synchronized (ackWaitLock) {

                if (this.commandName.equals(commandName)) {

                    if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                        LOG.info("MessageAckThread : Received RIGHT Ack " +
                                commandName + " for " + this.commandName +
                                " : Command Ack");
                    }

                    ackWaitLock.setAcknowledged(true);
                } else {

                    if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                        LOG.info("MessageAckThread : Received WRONG Ack " +
                                commandName + " for " + this.commandName +
                                " : Command Ack");
                    }
                }

                ackWaitLock.notify();
            }

        }

        class AckWaitLock extends Object {

            private boolean acknowledged = false;

            public boolean isAcknowledged() {

                return acknowledged;
            }

            public void setAcknowledged(boolean acknowledged) {

                this.acknowledged = acknowledged;

                if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                    LOG.info("AckWaitLock : SetAcknowldge " + acknowledged);
                }
            }

        }
    }

    /**
     * Because we push the voice payload through without periodic ack's we need
     * a thread to count a timeout in case the other side goes down.  Time out
     * of 40 seconds while IN CALL should be enough for any relay to catch up.
     * A more elegant solution is needed though i'd still like to keep it simple.
     * With reliable pipes SIP type acks seem too much
     */
    class ReceivedMessageTimeoutThread extends Thread {

        public static final int SLEEP_TIME = 1000;

        public static final long DEFAULT_PAYLOAD_MESSAGE_TIMEOUT = 1000 * 40;

        public static final long MINIMUM_PAYLOAD_MESSAGE_TIMEOUT = 1000 * 40;

        private long payloadMessageTimeout = DEFAULT_PAYLOAD_MESSAGE_TIMEOUT;

        private boolean run = true;

        public ReceivedMessageTimeoutThread() {

            setPriority(Thread.MIN_PRIORITY);

            //this.setDaemon (true);

        }

        public long getPayloadMessageTimeout() {

            return payloadMessageTimeout;
        }

        public void setPayloadMessageTimeout(long payloadMessageTimeout) {

            if (payloadMessageTimeout >= MINIMUM_PAYLOAD_MESSAGE_TIMEOUT) {

                this.payloadMessageTimeout = payloadMessageTimeout;
            }
        }

        public void stopThread() {

            run = false;
        }

        public void run() {

            while (run) {

                try {

                    Thread.sleep(VoJxtaCallControl.SECOND);
                } catch (InterruptedException ix) {
                    ix.printStackTrace();
                }

                if (getProtocolState() == VoJxtaCallControl.SESSION_VOJXTA_HOLDING) {
                    continue;
                }

                long lastMessageReceived = 0;

                if (speakerControl != null) {

                    lastMessageReceived = speakerControl.getTimeOfLastVoiceMessage();
                }

                long currentTime = System.currentTimeMillis();

                if (getProtocolState() == VoJxtaCallControl.SESSION_VOJXTA_INCALL &&
                        (currentTime - lastMessageReceived) > getPayloadMessageTimeout()) {

                    if (Logging.SHOW_INFO && LOG.isLoggable(Level.INFO)) {
                        LOG.info(" diff " + (currentTime - lastMessageReceived) +
                                " > " + getPayloadMessageTimeout());
                    }

                    VoJxtaCallControl.this.localActionMessageGoneUnAcknowledged();
                }
            }

        }

    }


}
